#include <clang/Frontend/CompilerInstance.h>
#include <catch2/catch_all.hpp>

#include <data_layout.h>
#include <interface.h>
#include "common.h"

#include <fmt/format.h>

#include <string_view>

using Catch::Matchers::ContainsSubstring;

// run_tool will leak memory when the ToolAction throws an exception, so
// disable AddressSanitizer's leak detection
const char* __asan_default_options() {
  return "detect_leaks=0";
}

inline std::ostream& operator<<(std::ostream& os, TypeCompatibility compat) {
  if (compat == TypeCompatibility::Full) {
    os << "Compatible";
  } else if (compat == TypeCompatibility::Repackable) {
    os << "Repackable";
  } else if (compat == TypeCompatibility::None) {
    os << "Incompatible";
  } else {
    os << "(INVALID)";
  }
  return os;
}

class DataLayoutCompareActionForTest;

namespace {

struct Fixture {
  /**
   * Parses annotations from the input source and generates data layout descriptions from it.
   *
   * Input code with common definitions (types, functions, ...) should be specified in "prelude".
   * It will be prepended to "code" before processing and also to the generator output.
   */
  std::unique_ptr<DataLayoutCompareActionForTest> compute_data_layout(std::string_view prelude, std::string_view code, GuestABI);
};

} // namespace

class DataLayoutCompareActionForTest : public DataLayoutCompareAction {
  std::unordered_map<const clang::Type*, TypeCompatibility> type_compat_cache;

  // Persistent reference taken to enable accessing the ASTContext after CompilerInstance::ExecuteAction returns
  llvm::IntrusiveRefCntPtr<clang::ASTContext> ast_context;
  std::shared_ptr<clang::Preprocessor> preprocessor;

public:
  DataLayoutCompareActionForTest(std::unique_ptr<ABI> guest_layout)
    : DataLayoutCompareAction(*guest_layout)
    , guest_layout(std::move(guest_layout)) {}

  void ExecuteAction() override {
    AnalysisAction::ExecuteAction();

    ast_context = &getCompilerInstance().getASTContext();
    preprocessor = getCompilerInstance().getPreprocessorPtr();
    host_layout = ComputeDataLayout(*ast_context, types);
  }

  std::unique_ptr<ABI> guest_layout;
  std::unordered_map<const clang::Type*, TypeInfo> host_layout;

  TypeCompatibility GetTypeCompatibility(std::string_view type_name) {
    for (const auto& [type, _] : host_layout) {
      if (clang::QualType {type, 0}.getAsString() == type_name) {
        return DataLayoutCompareAction::GetTypeCompatibility(*ast_context, type, host_layout, type_compat_cache);
      }
    }

    throw std::runtime_error("No data layout information recorded for type \"" + std::string {type_name} + "\"");
  }
};

/**
 * Same as clang::FrontendActionFactory but takes an external FrontendAction
 * reference instead of constructing an internal one. Since the FrontendAction
 * lifetime may extend past this ToolAction, state captured by the
 * FrontendAction can be accessed after the ToolAction returns.
 */
class ThunkTestToolAction : public clang::tooling::ToolAction {
public:
  clang::FrontendAction& ScopedToolAction;

public:
  ThunkTestToolAction(clang::FrontendAction& action)
    : ScopedToolAction(action) {}
  ~ThunkTestToolAction() = default;

  // Same as FrontendActionFactory but keeps ScopedToolAction alive when returning
  bool runInvocation(std::shared_ptr<clang::CompilerInvocation> invocation, clang::FileManager* files,
                     std::shared_ptr<clang::PCHContainerOperations> pch, clang::DiagnosticConsumer* diag_consumer) override {

    auto diagnostics = clang::CompilerInstance::createDiagnostics(&invocation->getDiagnosticOpts(), diag_consumer, false);

    clang::CompilerInstance Compiler(std::move(pch));
    Compiler.setInvocation(std::move(invocation));
    Compiler.setFileManager(files);
    Compiler.createDiagnostics(diag_consumer, false);
    if (!Compiler.hasDiagnostics()) {
      return false;
    }
    Compiler.createSourceManager(*files);

    const bool Success = Compiler.ExecuteAction(ScopedToolAction);

    files->clearStatCache();
    return Success;
  }
};

std::unique_ptr<DataLayoutCompareActionForTest> Fixture::compute_data_layout(std::string_view prelude, std::string_view code, GuestABI guest_abi) {
  const std::string full_code = std::string {prelude} + std::string {code};

  // Compute guest data layout
  auto data_layout_analysis_factory = std::make_unique<AnalyzeDataLayoutActionFactory>();
  run_tool(*data_layout_analysis_factory, full_code, false, guest_abi);

  // Compute host data layout
  auto ScopedToolAction = std::make_unique<DataLayoutCompareActionForTest>(data_layout_analysis_factory->TakeDataLayout());
  run_tool(std::make_unique<ThunkTestToolAction>(*ScopedToolAction), full_code, false, std::nullopt);

  return ScopedToolAction;
}

static std::string FormatDataLayout(const std::unordered_map<const clang::Type*, TypeInfo>& layout) {
  std::string ret;

  for (const auto& [type, info] : layout) {
    auto basic_info = info.get_if_simple_or_struct();
    if (!basic_info) {
      continue;
    }

    ret += fmt::format("  Host entry {}: {} ({})\n", clang::QualType {type, 0}.getAsString().c_str(), basic_info->size_bits / 8,
                       basic_info->alignment_bits / 8);

    if (auto struct_info = info.get_if_struct()) {
      for (const auto& member : struct_info->members) {
        ret += fmt::format("    Offset {}-{}: {} {}{}\n", member.offset_bits / 8, (member.offset_bits + member.size_bits - 1) / 8,
                           member.type_name.c_str(), member.member_name.c_str(),
                           member.array_size ? fmt::format("[{}]", member.array_size.value()).c_str() : "");
      }
    }
  }

  return ret;
}

TEST_CASE_METHOD(Fixture, "DataLayout") {
  auto guest_abi = GENERATE(GuestABI::X86_32, GuestABI::X86_64);
  INFO(guest_abi);

  SECTION("Trivial") {
    auto action = compute_data_layout("#include <thunks_common.h>\n",
                                      "struct A { int a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    REQUIRE(action->guest_layout->contains("A"));

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
  }

  SECTION("Builtin types") {
    auto action = compute_data_layout("#include <thunks_common.h>\n",
                                      "struct A { char a; short b; int c; float d; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    REQUIRE(action->guest_layout->contains("A"));

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
    CHECK(action->GetTypeCompatibility("char") == TypeCompatibility::Full);
    CHECK(action->GetTypeCompatibility("short") == TypeCompatibility::Full);
    CHECK(action->GetTypeCompatibility("int") == TypeCompatibility::Full);
    CHECK(action->GetTypeCompatibility("float") == TypeCompatibility::Full);
  }

  SECTION("Padding after int16_t") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int16_t a; int32_t b; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
  }

  SECTION("Array of int16_t") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int16_t a[64]; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
  }

  const auto compat_full64_repackable32 = (guest_abi == GuestABI::X86_32 ? TypeCompatibility::Repackable : TypeCompatibility::Full);

  SECTION("Type with platform-dependent size (size_t)") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdlib>\n",
                                      "struct A { size_t a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("int64_t has stricter alignment requirements on 64-bit platforms") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int64_t a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->alignment_bits == (guest_abi == GuestABI::X86_32 ? 32 : 64));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Array of int64_t") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int64_t a[64]; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("int64_t with explicit alignment specification") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct alignas(8) A { int64_t a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->alignment_bits == 64);
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
  }

  SECTION("int64_t alignment requirements propagate to parent struct") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int32_t a; int32_t b; int64_t c; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->alignment_bits == (guest_abi == GuestABI::X86_32 ? 32 : 64));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Padding before int64_t member") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int32_t a; int64_t b; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->members[1].offset_bits == (guest_abi == GuestABI::X86_32 ? 32 : 64));

    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Padding at end of struct due to int64_t alignment (like VkMemoryHeap)") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { int64_t a; int32_t b; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->size_bits == (guest_abi == GuestABI::X86_32 ? 96 : 128));

    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Different struct definition between guest and host; different member order") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct A { int32_t a; int32_t b; };\n"
                                      "#else\n"
                                      "struct A { int32_t b; int32_t a; };\n"
                                      "#endif\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->members.at(0).member_name == "b");
    CHECK(action->guest_layout->at("A").get_if_struct()->members.at(1).member_name == "a");

    REQUIRE(!action->host_layout.empty());
    CHECK(action->host_layout.begin()->second.get_if_struct()->members.at(0).member_name == "a");
    CHECK(action->host_layout.begin()->second.get_if_struct()->members.at(1).member_name == "b");

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
  }

  SECTION("Different struct definition between guest and host; different member size") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct A { int32_t a; int32_t b; };\n"
                                      "#else\n"
                                      "struct A { int32_t a; int64_t b; };\n"
                                      "#endif\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->guest_layout->at("A").get_if_struct()->members.at(0).size_bits == 32);
    CHECK(action->guest_layout->at("A").get_if_struct()->members.at(1).size_bits == 64);

    REQUIRE(!action->host_layout.empty());
    CHECK(action->host_layout.begin()->second.get_if_struct()->members.at(0).size_bits == 32);
    CHECK(action->host_layout.begin()->second.get_if_struct()->members.at(1).size_bits == 32);

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
  }

  SECTION("Different struct definition between guest and host; completely different members") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct A { int32_t a; int32_t b; };\n"
                                      "#else\n"
                                      "struct A { int32_t c; int32_t d; };\n"
                                      "#endif\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::None);
  }

  SECTION("Different struct definition between guest and host; member missing from guest") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct A { int32_t a; int32_t b; };\n"
                                      "#else\n"
                                      "struct A { int32_t a; };\n"
                                      "#endif\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::None);
  }

  SECTION("Different struct definition between guest and host; member missing from host") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct A { int32_t a; };\n"
                                      "#else\n"
                                      "struct A { int32_t a; int32_t b; };\n"
                                      "#endif\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));

    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::None);
  }

  SECTION("Nesting structs of consistent data layout") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct C { int32_t a; int16_t b; };\n"
                                      "struct B { C a; int16_t b; };\n"
                                      "struct A { int32_t a; B b; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    REQUIRE(action->guest_layout->contains("B"));
    REQUIRE(action->guest_layout->contains("C"));
    CHECK(action->guest_layout->at("A").get_if_struct()->members.at(0).size_bits == 32);

    CHECK(action->GetTypeCompatibility("struct C") == TypeCompatibility::Full);
    CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Full);
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
  }

  SECTION("Nesting repackable structs by embedding") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct C { int32_t a; int32_t b; };\n"
                                      "#else\n"
                                      "struct C { int32_t b; int32_t a; };\n"
                                      "#endif\n"
                                      "struct B { C a; int16_t b; };\n"
                                      "struct A { int32_t a; B b; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    REQUIRE(action->guest_layout->contains("B"));
    REQUIRE(action->guest_layout->contains("C"));
    CHECK(action->guest_layout->at("A").get_if_struct()->size_bits == 128);
    CHECK(action->guest_layout->at("A").get_if_struct()->alignment_bits == 32);

    CHECK(action->GetTypeCompatibility("struct C") == TypeCompatibility::Repackable);
    CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Repackable);
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
  }

  SECTION("Embedded union type (like VkRenderingAttachmentInfo)") {
    SECTION("without annotation") {
      CHECK_THROWS_WITH(compute_data_layout("#include <thunks_common.h>\n"
                                            "#include <cstdint>\n",
                                            "union B { int32_t a; uint32_t b; };\n"
                                            "struct A { B a; };\n"
                                            "template<> struct fex_gen_type<A> {};\n",
                                            guest_abi),
                        ContainsSubstring("unannotated member") && ContainsSubstring("union type"));
    }

    SECTION("with annotation") {
      auto action = compute_data_layout("#include <thunks_common.h>\n"
                                        "#include <cstdint>\n",
                                        "union B { int32_t a; uint32_t b; };\n"
                                        "struct A { B a; };\n"
                                        "template<> struct fex_gen_type<B> : fexgen::assume_compatible_data_layout {};\n"
                                        "template<> struct fex_gen_type<A> {};\n",
                                        guest_abi);

      INFO(FormatDataLayout(action->host_layout));

      REQUIRE(action->guest_layout->contains("A"));
      CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
    }
  }
}

TEST_CASE_METHOD(Fixture, "DataLayoutPointers") {
  auto guest_abi = GENERATE(GuestABI::X86_32, GuestABI::X86_64);
  INFO(guest_abi);

  const auto compat_full64_repackable32 = (guest_abi == GuestABI::X86_32 ? TypeCompatibility::Repackable : TypeCompatibility::Full);

  SECTION("Pointer to data with consistent layout") {
    std::string type = GENERATE("char", "short", "int", "float", "struct B { int a; }");
    INFO(type);
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { " + type +
                                        "* a; };\n"
                                        "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    // The pointer itself needs repacking on 32-bit. On 64-bit, no repacking is needed at all.
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
    if (!type.starts_with("struct B")) {
      CHECK(action->GetTypeCompatibility(type) == TypeCompatibility::Full);
    }
  }

  SECTION("Pointer to struct with consistent layout") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct B { int32_t a; };\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("B"));
    CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Full);
    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Unannotated pointer to incomplete type") {
    CHECK_THROWS_WITH(compute_data_layout("#include <thunks_common.h>\n"
                                          "#include <cstdint>\n",
                                          "struct B;\n"
                                          "struct A { B* a; };\n"
                                          "template<> struct fex_gen_type<A> {};\n",
                                          guest_abi),
                      ContainsSubstring("incomplete type"));
  }

  SECTION("Unannotated pointer to repackable type") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct B { int32_t a; int32_t b; };\n"
                                      "#else\n"
                                      "struct B { int32_t a; int64_t b; };\n"
                                      "#endif\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::None);
  }

  SECTION("Nesting repackable structs by pointers") {
    SECTION("Innermost struct is compatible") {
      auto action = compute_data_layout("#include <thunks_common.h>\n"
                                        "#include <cstdint>\n",
                                        "struct C { int32_t a; int32_t b; };\n"
                                        "struct B { C* a; int16_t b; };\n"
                                        "struct A { int32_t a; B b; };\n"
                                        "template<> struct fex_gen_type<A> {};\n",
                                        guest_abi);

      INFO(FormatDataLayout(action->host_layout));

      REQUIRE(action->guest_layout->contains("A"));
      REQUIRE(action->guest_layout->contains("B"));
      REQUIRE(action->guest_layout->contains("C"));

      // 64-bit is fully compatible, but 32-bit needs to zero-extend the pointer itself
      CHECK(action->GetTypeCompatibility("struct C") == TypeCompatibility::Full);
      CHECK(action->GetTypeCompatibility("struct B") == compat_full64_repackable32);
      CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
    }

    SECTION("Innermost struct is incompatible") {
      auto action = compute_data_layout("#include <thunks_common.h>\n"
                                        "#include <cstdint>\n",
                                        "#ifdef HOST\n"
                                        "struct C { int32_t a; int32_t b; };\n"
                                        "#else\n"
                                        "struct C { int32_t b; int32_t a; };\n"
                                        "#endif\n"
                                        "struct B { C* a; int16_t b; };\n"
                                        "struct A { int32_t a; B b; };\n"
                                        "template<> struct fex_gen_type<A> {};\n",
                                        guest_abi);

      INFO(FormatDataLayout(action->host_layout));

      REQUIRE(action->guest_layout->contains("A"));
      REQUIRE(action->guest_layout->contains("B"));
      REQUIRE(action->guest_layout->contains("C"));

      CHECK(action->GetTypeCompatibility("struct C") == TypeCompatibility::Repackable);
      CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::None);
      CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::None);
    }

    SECTION("Innermost struct is incompatible but the pointer member is annotated") {
      auto action = compute_data_layout("#include <thunks_common.h>\n"
                                        "#include <cstdint>\n",
                                        "#ifdef HOST\n"
                                        "struct C { int32_t a; int32_t b; };\n"
                                        "#else\n"
                                        "struct C { int32_t b; int32_t a; };\n"
                                        "#endif\n"
                                        "struct B { C* a; int16_t b; };\n"
                                        "struct A { int32_t a; B b; };\n"
                                        "template<> struct fex_gen_config<&B::a> : fexgen::custom_repack {};\n"
                                        "template<> struct fex_gen_type<A> {};\n"
                                        "template<> struct fex_gen_type<C> {};\n",
                                        guest_abi);

      INFO(FormatDataLayout(action->host_layout));

      REQUIRE(action->guest_layout->contains("A"));
      REQUIRE(action->guest_layout->contains("B"));
      REQUIRE(action->guest_layout->contains("C"));

      CHECK(action->GetTypeCompatibility("struct C") == TypeCompatibility::Repackable);
      CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Repackable);
      CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
    }
  }

  SECTION("Unannotated pointer to union type") {
    CHECK_THROWS_WITH(compute_data_layout("#include <thunks_common.h>\n"
                                          "#include <cstdint>\n",
                                          "union B { int32_t a; uint32_t b; };\n"
                                          "struct A { B* a; };\n"
                                          "template<> struct fex_gen_type<A> {};\n",
                                          guest_abi),
                      ContainsSubstring("unannotated member") && ContainsSubstring("union type"));
  }

  SECTION("Pointer to union type with assume_compatible_data_layout annotation") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "union B { int32_t a; uint32_t b; };\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_type<B> : fexgen::assume_compatible_data_layout {};\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Pointer to union type with custom_repack annotation") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "union B { int32_t a; uint32_t b; };\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_config<&A::a> : fexgen::custom_repack {};\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
  }

  SECTION("Pointer to opaque type") {
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct B;\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_type<B> : fexgen::opaque_type {};\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == compat_full64_repackable32);
  }

  SECTION("Pointer member with custom repacking code") {
    // Data layout analysis only needs to know about the custom_repack
    // annotation. The actual custom repacking code isn't needed for the
    // test.

    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "#ifdef HOST\n"
                                      "struct B { int32_t a; };\n"
                                      "#else\n"
                                      "struct B { int32_t b; };\n"
                                      "#endif\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_config<&A::a> : fexgen::custom_repack {};\n"
                                      "template<> struct fex_gen_type<A> {};\n"
                                      "template<> struct fex_gen_type<B> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    REQUIRE(action->guest_layout->contains("B"));
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
    CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::None);
  }

  SECTION("Custom repacking induces repacking requirement") {
    // Data layout analysis only needs to know about the custom_repack
    // annotation. The actual custom repacking code isn't needed for the
    // test.

    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct B {};\n"
                                      "struct A { B* a; };\n"
                                      "template<> struct fex_gen_config<&A::a> : fexgen::custom_repack {};\n"
                                      "template<> struct fex_gen_type<A> {};\n"
                                      "template<> struct fex_gen_type<B> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    REQUIRE(action->guest_layout->contains("B"));
    CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Repackable);
    CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Full);
  }

  SECTION("Self-referencing struct (like VkBaseOutStructure)") {
    // Without annotation
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { A* a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK_THROWS_WITH(action->GetTypeCompatibility("struct A"), ContainsSubstring("recursive reference"));

    // With annotation
    if (guest_abi == GuestABI::X86_64) {
      auto action = compute_data_layout("#include <thunks_common.h>\n"
                                        "#include <cstdint>\n",
                                        "struct A { A* a; };\n"
                                        "template<> struct fex_gen_type<A> : fexgen::assume_compatible_data_layout {};\n",
                                        guest_abi);

      INFO(FormatDataLayout(action->host_layout));

      REQUIRE(action->guest_layout->contains("A"));
      CHECK(action->GetTypeCompatibility("struct A") == TypeCompatibility::Full);
    }
  }

  SECTION("Circularly referencing structs") {
    // Without annotation
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct B;\n"
                                      "struct A { B* a; };\n"
                                      "struct B { A* a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    REQUIRE(action->guest_layout->contains("B"));
    CHECK_THROWS_WITH(action->GetTypeCompatibility("struct A"), ContainsSubstring("recursive reference"));
    CHECK_THROWS_WITH(action->GetTypeCompatibility("struct B"), ContainsSubstring("recursive reference"));

    // With annotation
    if (guest_abi == GuestABI::X86_64) {
      auto action = compute_data_layout("#include <thunks_common.h>\n"
                                        "#include <cstdint>\n",
                                        "struct B;\n"
                                        "struct A { B* a; };\n"
                                        "struct B { A* a; };\n"
                                        "template<> struct fex_gen_type<B> : fexgen::assume_compatible_data_layout {};\n",
                                        guest_abi);

      INFO(FormatDataLayout(action->host_layout));

      REQUIRE(action->guest_layout->contains("B"));
      CHECK(action->GetTypeCompatibility("struct B") == TypeCompatibility::Full);
    }
  }

  SECTION("Pointers to void") {
    // Without annotation
    auto action = compute_data_layout("#include <thunks_common.h>\n"
                                      "#include <cstdint>\n",
                                      "struct A { void* a; };\n"
                                      "template<> struct fex_gen_type<A> {};\n",
                                      guest_abi);

    INFO(FormatDataLayout(action->host_layout));

    REQUIRE(action->guest_layout->contains("A"));
    CHECK(action->GetTypeCompatibility("struct A") == (guest_abi == GuestABI::X86_32 ? TypeCompatibility::None : TypeCompatibility::Full));
  }

  // TODO: Double pointers to compatible data: struct B { int a ; }; struct A { B** b; };
}
